package com.gusto.engine.beans.model.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Configurable;

import com.gusto.engine.beans.annotations.Comparable;
import com.gusto.engine.beans.annotations.Id;
import com.gusto.engine.beans.exceptions.PropertyAccessException;
import com.gusto.engine.beans.exceptions.ResCreationException;
import com.gusto.engine.beans.model.Res;

/**
 * <p>The ObjectModel Resource {@link ObjectModelRes} defines a POJO as a resource.<br/>
 * It defines the methods allowing to access value, resource and set properties.<br/>
 * To create a resource use the {@code Res createResource(object)} method 
 * and pass a POJO object annotated with the &#64;Comparable annotation.</p>
 * 
 * @author amokrane.belloui@gmail.com
 */
@Configurable
public class ObjectModelRes implements Res {
	 
	private Logger log = Logger.getLogger(getClass());
	
	private Object object;
	
	private ObjectModelRes() {
		super();
	}
	
	private ObjectModelRes(Object object) {
		super();
		this.object = object;
	}
	
	/**
	 * Factory method for creating a resource.
	 * @param object POJO object annotated with &#64;{@link Comparable} and &#64;{@link Id}
	 * @param check check / do not check, if the object is annotated
	 * @return 
	 * @throws ResCreationException
	 */
	public static Res createResource(Object object, boolean check) throws ResCreationException {
		ObjectModelRes res = new ObjectModelRes(object);
		if (check) {
			res.checkObject();
		}
		return res;
	}
	
	/**
	 * Factory method for creating a resource, the check is implicit.
	 * @param object POJO object annotated with &#64;{@link Comparable} and &#64;{@link Id}
	 * @return {@link Res}
	 * @throws ResCreationException
	 */
	public static Res createResource(Object object) throws ResCreationException {
		ObjectModelRes res = new ObjectModelRes(object);
		res.checkObject();
		return res;
	}
	
	private void checkValue(Object o) throws ResCreationException {
		Class<? extends Object> theClass = o.getClass();
		boolean isCombarable = theClass.isAnnotationPresent(Comparable.class);
		boolean isId = false;
		try {
			getIdField(theClass);
			isId = true;
		} 
		catch (Exception e) {
			isId = false;
		}
		log.debug("@Comparable : " + isCombarable );
		log.debug("@Id : " + isId );
		if (isCombarable) {
			throw new ResCreationException("Class " + theClass.getName() + " should be of a simple type, not @Comparable");
		}
		if (isId) {
			throw new ResCreationException("Class " + theClass.getName() + " should be of a simple type, shouldn't declare @Id fields");
		}
	}
	
	private void checkObject() throws ResCreationException {
		Class<? extends Object> theClass = this.object.getClass();
		boolean isCombarable = theClass.isAnnotationPresent(Comparable.class);
		boolean isId = false;
		try {
			getIdField(theClass);
			isId = true;
		} 
		catch (Exception e) {
			isId = false;
		}
		log.debug("@Comparable : " + isCombarable );
		log.debug("@Id : " + isId );
		if (!isCombarable) {
			throw new ResCreationException("Class " + theClass.getName() + " should be annotated with the @Comparable annotation");
		}
		if (!isId) {
			throw new ResCreationException("No field of " + theClass.getName() + " is annotated with the @Id annotation");
		}
	}
	
	private List<Field> getIdField(Class<? extends Object> theClass) throws Exception {
		List<Field> idAnnotated = new ArrayList<Field>();
		
		for (Field f : theClass.getDeclaredFields()) {
			if (f.isAnnotationPresent(Id.class)) {
				idAnnotated.add(f);
			}
		}
		if (idAnnotated.size() > 0) {
			return idAnnotated;
		}
		else {
			throw new Exception("No @Id field declared");
		}
	}
	
	private String getGetter(String property) {
		return "get" + property.substring(0,1).toUpperCase() + property.substring(1);
	}
	
	public String toString() {
		try {
			String id = "";
			for (Field f : getIdField(this.object.getClass())) {
				Object val = this.getProp(f.getName());
				id += f.getName() + ":" + val + ";";
			}
			
			return object.getClass().getName() + "[" + id + "]";
		}
		catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	
	private Object getObject() {
		return object;
	}
	
	public boolean equals(Object anObject) {
		if (anObject instanceof ObjectModelRes) {
			ObjectModelRes res = (ObjectModelRes)anObject;
			return this.object.equals(res.getObject());
		}
		return false;
	}
	
	private Object getProp(String property) throws PropertyAccessException {
		try {
			Class<? extends Object> c = object.getClass();
			Method method = c.getMethod(getGetter(property));
			Object value = method.invoke(object);
			return value;
		}
		catch (Exception ex) {
			throw new PropertyAccessException("Property " + property + " not found on object of class " + object.getClass(), ex);
		}
	}
	
	@SuppressWarnings("unchecked")
	private List<Object> getListProp(String property) throws PropertyAccessException {
		Object value = getProp(property);
		List<Object> list;
		try {
			list = (List<Object>)value;
		}
		catch (Exception ex) {
			throw new PropertyAccessException("The property " + property + " of class " + object.getClass() + " is not a list property", ex);
		}
		return list;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Object getValueProperty(String property) throws PropertyAccessException {
		Object value = getProp(property);
		try {
			this.checkValue(value);
		} 
		catch (Exception ex) {
			throw new PropertyAccessException("Cannot get the value property ", ex);
		}
		return value;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Res getResourceProperty(String property) throws PropertyAccessException {
		Object value = getProp(property);
		ObjectModelRes res = new ObjectModelRes(value);
		try {
			res.checkObject();
		} 
		catch (Exception ex) {
			throw new PropertyAccessException("Cannot get the resource property ", ex);
		}
		return res;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<Object> getValueSetProperty(String property) throws PropertyAccessException {
		List<Object> list = getListProp(property);
		// Check
		try {
			for (Object o : list) {
				this.checkValue(o);
			}
		} 
		catch (Exception ex) {
			throw new PropertyAccessException("Cannot get the value set property ", ex);
		}
		return list;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public List<Res> getResourceSetProperty(String property) throws PropertyAccessException {
		List<Object> list = getListProp(property);
		List<Res> result = new ArrayList<Res>();
		// Add and Check
		try {
			for (Object o : list) {
				ObjectModelRes res = new ObjectModelRes(o);
				res.checkObject();
				result.add(res);
			}
		}
		catch (Exception ex) {
			throw new PropertyAccessException("Cannot get the resource set property ", ex);
		}
		return result;
	}
	
}
